/*
 * $Id: CalendarHeaderHandler.java 3927 2011-02-22 16:34:11Z kleopatra $
 *
 * Copyright 2007 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
package org.jdesktop.swingx.plaf.basic;

import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.logging.Logger;

import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.UIManager;
import javax.swing.plaf.UIResource;

import org.jdesktop.swingx.JXMonthView;
import org.jdesktop.swingx.action.AbstractActionExt;

/**
 * Provides and wires a component appropriate as a calendar navigation header.
 * The design idea is to support a pluggable header for a zoomable (PENDING JW:
 * naming!) JXMonthView. Then custom implementations can be tailored to exactly
 * fit their needs.
 * <p>
 * 
 * To install a custom implementation, register the class name of the custom
 * header handler with the key <code>CalendarHeaderHandler.uiControllerID</code>
 * , example:
 * 
 * <pre>
 * <code>
 *  UIManager.put(CalendarHeaderHandler.uiControllerID, &quot;com.foo.bar.MagicHeaderHandler&quot;)
 * </code>
 * </pre>
 * 
 * Basic navigation action should (will) be defined by the ui delegate itself
 * (PENDING JW: still incomplete in BasicMonthViewUI). This handler can
 * modify/enhance them as appropriate for its context.
 * <p>
 * 
 * PENDING JW: those icons ... who's responsible? Shouldn't we use any of the
 * default arrows as defined in the laf anyway (are there any?)
 * <p>
 * 
 * <b>Note</b>: this is work-in-progress, be prepared to change if subclassing
 * for custom requirements!
 * 
 * @author Jeanette Winzenburg
 */
public abstract class CalendarHeaderHandler {

	@SuppressWarnings("unused")
	private static final Logger LOG = Logger.getLogger(CalendarHeaderHandler.class.getName());

	public static final String uiControllerID = "CalendarHeaderHandler";

	protected JXMonthView monthView;

	private JComponent calendarHeader;

	protected Icon monthDownImage;

	protected Icon monthUpImage;

	private PropertyChangeListener monthViewPropertyChangeListener;

	/**
	 * Installs this handler to the given month view.
	 * 
	 * @param monthView
	 *            the target month view to install to.
	 */
	public void install(JXMonthView monthView) {
		this.monthView = monthView;
		// PENDING JW: remove here if rendererHandler takes over control
		// completely
		// as is, some properties are duplicated
		monthDownImage = UIManager.getIcon("JXMonthView.monthDownFileName");
		monthUpImage = UIManager.getIcon("JXMonthView.monthUpFileName");
		installNavigationActions();
		installListeners();
		componentOrientationChanged();
		monthStringBackgroundChanged();
		fontChanged();
	}

	/**
	 * Uninstalls this handler from the given target month view.
	 * 
	 * @param monthView
	 *            the target month view to install from.
	 */
	public void uninstall(JXMonthView monthView) {
		this.monthView.remove(getHeaderComponent());
		uninstallListeners();
		this.monthView = null;
	}

	/**
	 * Returns a component to be used as header in a zoomable month view,
	 * guaranteed to be not null.
	 * 
	 * @return a component to be used as header in a zoomable JXMonthView
	 */
	public JComponent getHeaderComponent() {
		if (calendarHeader == null) {
			calendarHeader = createCalendarHeader();
		}
		return calendarHeader;
	}

	/**
	 * Creates and registered listeners on the monthView as appropriate. This
	 * implementation registers a PropertyChangeListener which synchronizes
	 * internal state on changes of componentOrientation, font and
	 * monthStringBackground.
	 */
	protected void installListeners() {
		monthView.addPropertyChangeListener(getMonthViewPropertyChangeListener());
	}

	/**
	 * Unregisters listeners which had been installed to the monthView.
	 */
	protected void uninstallListeners() {
		monthView.removePropertyChangeListener(monthViewPropertyChangeListener);
	}

	/**
	 * Returns the propertyChangelistener for the monthView. Lazily created.
	 * 
	 * @return the propertyChangeListener for the monthView.
	 */
	private PropertyChangeListener getMonthViewPropertyChangeListener() {
		if (monthViewPropertyChangeListener == null) {
			monthViewPropertyChangeListener = new PropertyChangeListener() {

				@Override
				public void propertyChange(PropertyChangeEvent evt) {
					if ("componentOrientation".equals(evt.getPropertyName())) {
						componentOrientationChanged();
					} else if ("font".equals(evt.getPropertyName())) {
						fontChanged();
					} else if ("monthStringBackground".equals(evt.getPropertyName())) {
						monthStringBackgroundChanged();
					}

				}
			};
		}
		return monthViewPropertyChangeListener;
	}

	/**
	 * Synchronizes internal state which depends on the month view's
	 * monthStringBackground.
	 */
	protected void monthStringBackgroundChanged() {
		getHeaderComponent().setBackground(getAsNotUIResource(monthView.getMonthStringBackground()));

	}

	/**
	 * Synchronizes internal state which depends on the month view's font.
	 */
	protected void fontChanged() {
		getHeaderComponent().setFont(getAsNotUIResource(createDerivedFont()));
		monthView.revalidate();
	}

	/**
	 * Synchronizes internal state which depends on the month view's
	 * componentOrientation.
	 * 
	 * This implementation updates the month navigation icons and the header
	 * component's orientation.
	 */
	protected void componentOrientationChanged() {
		getHeaderComponent().applyComponentOrientation(monthView.getComponentOrientation());
		if (monthView.getComponentOrientation().isLeftToRight()) {
			updateMonthNavigationIcons(monthDownImage, monthUpImage);
		} else {
			updateMonthNavigationIcons(monthUpImage, monthDownImage);
		}
	}

	/**
	 * @param previous
	 *            the icon to use in the previousMonth action
	 * @param next
	 *            the icon to use on the nextMonth action
	 */
	private void updateMonthNavigationIcons(Icon previous, Icon next) {
		updateActionIcon("previousMonth", previous);
		updateActionIcon("nextMonth", next);
	}

	/**
	 * @param previousKey
	 * @param previous
	 */
	private void updateActionIcon(String previousKey, Icon previous) {
		Action action = monthView.getActionMap().get(previousKey);
		if (action != null) {
			action.putValue(Action.SMALL_ICON, previous);
		}
	}

	/**
	 * Creates and returns the component used as header in a zoomable monthView.
	 * 
	 * @return the component used as header in a zoomable monthView, guaranteed
	 *         to be not null.
	 */
	protected abstract JComponent createCalendarHeader();

	/**
	 * Installs and configures navigational actions.
	 * <p>
	 * 
	 * This implementation creates and installs wrappers around the
	 * scrollToPrevious/-NextMonth actions installed by the ui and configures
	 * them with the appropriate next/previous icons.
	 */
	protected void installNavigationActions() {
		installWrapper("scrollToPreviousMonth", "previousMonth",
				monthView.getComponentOrientation().isLeftToRight() ? monthDownImage : monthUpImage);
		installWrapper("scrollToNextMonth", "nextMonth", monthView.getComponentOrientation().isLeftToRight() ? monthUpImage
				: monthDownImage);
	}

	/**
	 * Creates an life action wrapper around the action registered with
	 * actionKey, sets its SMALL_ICON property to the given icon and installs
	 * itself with the newActionKey.
	 * 
	 * @param actionKey
	 *            the key of the action to wrap around
	 * @param newActionKey
	 *            the key of the wrapper action
	 * @param icon
	 *            the icon to use in the wrapper action
	 */
	private void installWrapper(final String actionKey, String newActionKey, Icon icon) {
		AbstractActionExt wrapper = new AbstractActionExt(null, icon) {

			@Override
			public void actionPerformed(ActionEvent e) {
				Action action = monthView.getActionMap().get(actionKey);
				if (action != null) {
					action.actionPerformed(e);
				}
			}

		};
		monthView.getActionMap().put(newActionKey, wrapper);
	}

	/**
	 * Returns a Font based on the param which is not of type UIResource.
	 * 
	 * @param font
	 *            the base font
	 * @return a font not of type UIResource, may be null.
	 */
	private Font getAsNotUIResource(Font font) {
		if (!(font instanceof UIResource))
			return font;
		// PENDING JW: correct way to create another font instance?
		return font.deriveFont(font.getAttributes());
	}

	/**
	 * Returns a Color based on the param which is not of type UIResource.
	 * 
	 * @param color
	 *            the base color
	 * @return a color not of type UIResource, may be null.
	 */
	private Color getAsNotUIResource(Color color) {
		if (!(color instanceof UIResource))
			return color;
		// PENDING JW: correct way to create another color instance?
		float[] rgb = color.getRGBComponents(null);
		return new Color(rgb[0], rgb[1], rgb[2], rgb[3]);
	}

	/**
	 * Create a derived font used to when painting various pieces of the month
	 * view component. This method will be called whenever the font on the
	 * component is set so a new derived font can be created.
	 */
	protected Font createDerivedFont() {
		return monthView.getFont().deriveFont(Font.BOLD);
	}

}
